using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CSharp.RuntimeBinder;

namespace Charisma.Metadata
{
    [DebuggerDisplay("{Type}")]
    public sealed class MetaType
    {
        private readonly Lazy<ConcurrentDictionary<string, Func<object, object>>> getCache;
        private readonly Lazy<Expression> parameter;
        private readonly Lazy<ConcurrentDictionary<string, Action<object, object>>> setCache;

        internal MetaType(Type type, Func<object[], object> createInstance, Func<object, object> handleMaterialization)
        {
            Contract.Requires(type != null);
            Contract.Requires(type.IsInterface || type.IsAbstract || createInstance != null);

            Type = type;
            CreateInstance = createInstance;
            HandleMaterialization = handleMaterialization;
            IsDynamic = typeof (IDynamicMetaObjectProvider).IsAssignableFrom(type);
            IsArray = type.IsArray;
            Fields = new MetaFieldCollection();
            parameter = new Lazy<Expression>(() => Expression.Parameter(type), LazyThreadSafetyMode.PublicationOnly);
            getCache =
                new Lazy<ConcurrentDictionary<string, Func<object, object>>>(
                    () => new ConcurrentDictionary<string, Func<object, object>>(2, 8));
            setCache =
                new Lazy<ConcurrentDictionary<string, Action<object, object>>>(
                    () => new ConcurrentDictionary<string, Action<object, object>>(2, 8));
        }

        public Type Type { get; private set; }
        public bool IsDynamic { get; private set; }
        public bool IsArray { get; private set; }
        public ReadOnlyCollection<string> ConstructorArguments { get; internal set; }
        public Func<object[], object> CreateInstance { get; private set; }
        public Func<object, object> HandleMaterialization { get; private set; }
        public MetaField KeyField { get; internal set; }
        public MetaFieldCollection Fields { get; private set; }

        public IEnumerable<string> GetDynamicMemberNames(object instance)
        {
            if (!IsDynamic || instance == null)
                return Enumerable.Empty<string>();
            var provider = (IDynamicMetaObjectProvider) instance;
            DynamicMetaObject metaObject = provider.GetMetaObject(parameter.Value);
            return metaObject.GetDynamicMemberNames()
                .Where(n => !Fields.Contains(n))
                .ToList();
        }

        public MetaField GetDynamicAccessor(string name)
        {
            if (!IsDynamic || Fields.Contains(name))
                return null;
            Func<object, object> getter = getCache.Value.GetOrAdd(name, GenerateGetter);
            Action<object, object> setter = setCache.Value.GetOrAdd(name, GenerateSetter);
            return new MetaField(this, name, getter, setter);
        }

        private Func<object, object> GenerateGetter(string name)
        {
            var argumentInfo = new[] {CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)};
            CallSiteBinder binder = Binder.GetMember(CSharpBinderFlags.None, name, Type, argumentInfo);
            CallSite<Func<CallSite, object, object>> callSite = CallSite<Func<CallSite, object, object>>.Create(binder);
            Func<object, object> result = t => callSite.Target(callSite, t);
            return result;
        }

        private Action<object, object> GenerateSetter(string name)
        {
            var argumentInfo = new[]
                                   {
                                       CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
                                       CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.Constant, null)
                                   };
            CallSiteBinder binder = Binder.SetMember(CSharpBinderFlags.None, name, Type, argumentInfo);
            CallSite<Action<CallSite, object, object>> callSite =
                CallSite<Action<CallSite, object, object>>.Create(binder);
            Action<object, object> result = (t, v) => callSite.Target(callSite, t, v);
            return result;
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(Type != null);
            Contract.Invariant(parameter != null);
            Contract.Invariant(getCache != null);
            Contract.Invariant(setCache != null);
            Contract.Invariant(Fields != null);
            Contract.Invariant(Type.IsInterface || Type.IsAbstract || CreateInstance != null);
        }
    }
}